package vip.xiaonuo.modular.file.util;

import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.ObjectUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import sun.misc.Cleaner;
import vip.xiaonuo.core.exception.ServiceException;
import vip.xiaonuo.modular.file.MultipartFileParam;
import vip.xiaonuo.modular.file.resp.UploadStatusResp;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.lang.reflect.Method;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

/**
 * FileChunkUtils
 *
 * @author JackLee
 * 2022/7/28 14:42
 */
@Slf4j
public class FileChunkUtils {


    /**
     * 分片上传
     *
     * @param param
     */
    public static UploadStatusResp uploadByMappedByteBuffer(MultipartFileParam param) {
        if (StringUtils.isBlank(param.getIdentifier())) {
            param.setIdentifier(UUID.randomUUID().toString());
        }
        UploadStatusResp resp = new UploadStatusResp();
        FileChannel fileChannel = null;
        RandomAccessFile raf = null;
        String fileName;
        String tempFileName;
        String filePath;
        File tempFile;
        try {
            if (ObjectUtil.isEmpty(param.getFile())) {
                return checkUploadStatus(param);
            }
            //文件名称
            fileName = getFileName(param);
            //临时文件
            tempFileName = param.getIdentifier() + fileName.substring(fileName.lastIndexOf(".")) + "_tmp";
            //文件路径
            filePath = getUploadPath(param);
            //创建临时文件
            tempFile = CommonFileUtils.getAbsoluteFile(filePath, tempFileName);
            //第一步 获取RandomAccessFile,随机访问文件类的对象
            raf = new RandomAccessFile(tempFile, "rw");
            //第二步 调用RandomAccessFile的getChannel()方法，打开文件通道 FileChannel
            fileChannel = raf.getChannel();
            //第三步 获取当前是第几个分块，计算文件的最后偏移量
            long offset = (param.getChunkNumber() - 1) * param.getChunkSize();
            //第四步 获取当前文件分块的字节数组，用于获取文件字节长度
            byte[] fileData = param.getFile().getBytes();
            //第五步 使用文件通道FileChannel类的 map（）方法创建直接字节缓冲器  MappedByteBuffer
            MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, offset, fileData.length);
            //第六步 将分块的字节数组放入到当前位置的缓冲区内  mappedByteBuffer.put(byte[] b)
            mappedByteBuffer.put(fileData);
            //第七步 释放缓冲区
            freeMappedByteBuffer(mappedByteBuffer);
        } catch (Exception e) {
            log.error("分片上传文件错误", e);
            throw new ServiceException(500101, "分片上传文件错误！");
        } finally {
            try {
                fileChannel.close();
                raf.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }


        //第八步 检查文件是否全部完成上传
        boolean isComplete = false;
        try {
            isComplete = checkUploadStatus(param, fileName, filePath);
        } catch (IOException e) {
            log.error("检查文件是否全部完成上传错误", e);
            throw new ServiceException(500102, "检查文件是否完成上传失败！");
        }
        if (isComplete) {
            // 完成后，临时文件命名为正式文件名
            renameFile(tempFile, fileName);
        }
        resp.filePath(StringUtils.join(filePath, File.separator, fileName));
        resp.fileName(param.getFile().getOriginalFilename());
        resp.identifier(param.getIdentifier());
        resp.endUpload(isComplete);
        log.info("上传【{}】文件，{}完成", fileName, isComplete ? "已经" : "还未");
        return resp;
    }

    private static boolean renameFile(File toBeRenamed, String toFileNewName) {
        //检查要重命名的文件是否存在，是否是文件
        if (!toBeRenamed.exists() || toBeRenamed.isDirectory()) {
            return false;
        }
        String p = toBeRenamed.getParent();
        File newFile = new File(p + File.separatorChar + toFileNewName);
        //修改文件名
        return toBeRenamed.renameTo(newFile);
    }

    private static boolean checkUploadStatus(MultipartFileParam param, String fileName, String filePath) throws IOException {
        // 校验conf文件
        File confFile = checkConfFile(fileName, filePath);
        // 读取conf
        RandomAccessFile confAccessFile = new RandomAccessFile(confFile, "rw");
        //设置文件长度
        if (confAccessFile.length() != param.getTotalChunks()) {
            confAccessFile.setLength(param.getTotalChunks());
        }
        //设置起始偏移量
        confAccessFile.seek(param.getChunkNumber() - 1);
        //将指定的一个字节写入文件中 127，
        confAccessFile.write(Byte.MAX_VALUE);
        confAccessFile.close();
        byte[] completeStatusList = FileUtil.readBytes(confFile);
        byte isComplete = Byte.MAX_VALUE;
        //这一段逻辑有点复杂，看的时候思考了好久，创建conf文件文件长度为总分片数，每上传一个分块即向conf文件中写入一个127，那么没上传的位置就是默认的0,已上传的就是Byte.MAX_VALUE 127
        for (int i = 0; i < completeStatusList.length && isComplete == Byte.MAX_VALUE; i++) {
            // 按位与运算，将&两边的数转为二进制进行比较，有一个为0结果为0，全为1结果为1  eg.3&5  即 0000 0011 & 0000 0101 = 0000 0001   因此，3&5的值得1。
            isComplete = (byte) (isComplete & completeStatusList[i]);
        }
        if (isComplete == Byte.MAX_VALUE) {
            //如果全部文件上传完成，删除conf文件
//            FileUtil.del(confFile.getAbsolutePath());//不能删除，需要用conf来判断文件
            return true;
        }
        return false;
    }

    /**
     * 检查上传状态
     *
     * @param param
     * @return
     * @throws IOException
     */
    public static UploadStatusResp checkUploadStatus(MultipartFileParam param) throws IOException {
        String fileName = getFileName(param);
        File confFile = checkConfFile(fileName, getUploadPath(param));
        byte[] completeStatusList = FileUtil.readBytes(confFile);
        List<String> uploadeds = new ArrayList<>();
        for (int i = 0; i < completeStatusList.length; i++) {
            if (completeStatusList[i] == Byte.MAX_VALUE) {
                uploadeds.add(i + 1 + "");
            }
        }
        UploadStatusResp resp = new UploadStatusResp();
        resp.uploaded(uploadeds)
                .skipUpload(completeStatusList.length > 0 && completeStatusList.length == uploadeds.size());
        if (ObjectUtil.isEmpty(completeStatusList)) {
            resp.chunk(false);
            return resp;
        }
        if (completeStatusList.length < param.getChunkNumber()) {
            resp.chunk(false);
            return resp;
        }
        byte b = completeStatusList[param.getChunkNumber() - 1];
        if (b != Byte.MAX_VALUE) {
            resp.chunk(false);
            return resp;
        }
        resp.chunk(true);
        resp.filePath(StringUtils.join(getUploadPath(param), File.separator, fileName));
        return resp;
    }


    private static String getFileName(MultipartFileParam param) {
        String extension;
        if (ObjectUtil.isNotEmpty(param.getFile())) {
            String filename = param.getFile().getOriginalFilename();
            extension = filename.substring(filename.lastIndexOf("."));
        } else {
            extension = param.getFilename().substring(param.getFilename().lastIndexOf("."));
        }
        return param.getIdentifier() + extension;
    }

    private static String getUploadPath(MultipartFileParam param) {
        return CommonFileUtils.fileRootPath + param.getObjectType();
    }

    private static File checkConfFile(String fileName, String filePath) throws IOException {
        File confFile = CommonFileUtils.getAbsoluteFile(filePath, fileName + ".conf");
        if (!confFile.exists()) {
            confFile.createNewFile();
        }
        return confFile;
    }

    private static void freeMappedByteBuffer(final MappedByteBuffer mappedByteBuffer) {
        try {
            if (mappedByteBuffer == null) {
                return;
            }
            mappedByteBuffer.force();
            AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
                try {
                    Method getCleanerMethod = mappedByteBuffer.getClass().getMethod("cleaner", new Class[0]);
                    //可以访问private的权限
                    getCleanerMethod.setAccessible(true);
                    //在具有指定参数的 方法对象上调用此 方法对象表示的底层方法
                    Cleaner cleaner = (Cleaner) getCleanerMethod.invoke(mappedByteBuffer,
                            new Object[0]);
                    cleaner.clean();
                } catch (Exception e) {
                    log.error("clean MappedByteBuffer error!!!", e);
                }
                return null;
            });
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
